Passed
Pull Request — master (#136)
by
unknown
02:10
created

Frame.ts ➔ getDataLength   A

Complexity

Conditions 2

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 2
1
import zlib = require('zlib')
2
import {
3
    Flags,
4
    getHeaderSize,
5
    FrameHeader
6
} from './FrameHeader'
7
import * as Frames from './Frames'
8
import * as ID3Util from './ID3Util'
9
import { isKeyOf } from "./util"
10
11
export class Frame {
12
    identifier: string
13
    private value: unknown
14
    flags: Flags
15
16
    constructor(identifier: string, value: unknown, flags: Flags = {}) {
17
        this.identifier = identifier
18
        this.value = value
19
        this.flags = flags
20
    }
21
22
    static createFromBuffer(
23
        frameBuffer: Buffer,
24
        version: number
25
    ): Frame | null {
26
        const headerSize = getHeaderSize(version)
27
        // Specification requirement
28
        if (frameBuffer.length < headerSize + 1) {
29
            return null
30
        }
31
        const headerBuffer = frameBuffer.subarray(0, headerSize)
32
        const header = FrameHeader.createFromBuffer(headerBuffer, version)
33
        if (header.flags.encryption) {
34
            return null
35
        }
36
37
        let body = getBody(
38
            header.flags,
39
            frameBuffer,
40
            headerSize,
41
            header.bodySize
42
        )
43
        const dataLength = getDataLength(header.flags, frameBuffer, headerSize)
44
        const decompressedBody = decompressBody(header.flags, dataLength, body)
45
        if (!decompressedBody) {
46
            return null
47
        }
48
        body = decompressedBody
49
50
        const identifier = header.identifier
51
        let value = null
52
        if (isKeyOf(identifier, Frames.Frames)) {
53
            value = Frames.Frames[identifier].read(body, version)
54
        } else if (identifier.startsWith('T')) {
55
            value = Frames.GENERIC_TEXT.read(body)
56
        } else if (identifier.startsWith('W')) {
57
            value = Frames.GENERIC_URL.read(body)
58
        } else {
59
            return null
60
        }
61
        return new Frame(identifier, value, header.flags)
62
    }
63
64
    getBuffer() {
65
        if (isKeyOf(this.identifier, Frames.Frames)) {
66
            return Frames.Frames[this.identifier].create(this.value)
67
        }
68
        if (this.identifier.startsWith('T')) {
69
            return Frames.GENERIC_TEXT.create(this.identifier, this.value)
70
        }
71
        if (this.identifier.startsWith('W')) {
72
            return Frames.GENERIC_URL.create(this.identifier, this.value)
73
        }
74
        return null
75
    }
76
77
    getValue() {
78
        return this.value
79
    }
80
}
81
82
function getBody(
83
    flags: Flags,
84
    buffer: Buffer,
85
    headerSize: number,
86
    bodySize: number
87
) {
88
    const bodyOffset = flags.dataLengthIndicator ? 4 : 0
89
    const bodyStart = headerSize + bodyOffset
90
    const bodyEnd = bodyStart + bodySize - bodyOffset
91
    const body = buffer.subarray(bodyStart, bodyEnd)
92
    if (flags.unsynchronisation) {
93
        // This method should stay in ID3Util for now because it's also used in the Tag's header which we don't have a class for.
94
        return ID3Util.processUnsynchronisedBuffer(body)
95
    }
96
    return body
97
}
98
99
function getDataLength(
100
    {dataLengthIndicator}: Flags,
101
    frameBuffer: Buffer,
102
    dataLengthOffset: number,
103
) {
104
    return dataLengthIndicator ? frameBuffer.readInt32BE(dataLengthOffset) : 0
105
}
106
107
function decompressBody(
108
    {compression}: Flags,
109
    dataLength: number,
110
    body: Buffer
111
) {
112
    return compression ? decompressBuffer(body, dataLength) : body
113
}
114
115
function decompressBuffer(buffer: Buffer, expectedDecompressedLength: number) {
116
    if (buffer.length < 5) {
117
        return null
118
    }
119
120
    // ID3 spec defines that compression is stored in ZLIB format,
121
    // but doesn't specify if header is present or not.
122
    // ZLIB has a 2-byte header.
123
    // 1. try if header + body decompression
124
    // 2. else try if header is not stored (assume that all content is deflated "body")
125
    // 3. else try if inflation works if the header is omitted (implementation dependent)
126
    const tryDecompress = () => {
127
        try {
128
            return zlib.inflateSync(buffer)
129
        } catch (error) {
130
            try {
131
                return zlib.inflateRawSync(buffer)
132
            } catch (error) {
133
                try {
134
                    return zlib.inflateRawSync(buffer.subarray(2))
135
                } catch (error) {
136
                    return null
137
                }
138
            }
139
        }
140
    }
141
    const decompressed = tryDecompress()
142
    if (decompressed && decompressed.length === expectedDecompressedLength) {
143
        return decompressed
144
    }
145
    return null
146
}
147